/**
 * 
 */
package org.bookie.framework.services.impl;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.hivemind.ApplicationRuntimeException;
import org.bookie.core.services.impl.Listener;
import org.bookie.core.services.impl.Listener.Event;
import org.bookie.framework.CommonBaseComponent;
import org.bookie.framework.CommonBasePage;
import org.bookie.framework.services.NotificationService;

import edu.emory.mathcs.backport.java.util.Arrays;

/**
 * Notification service implementation
 * 
 * @author zubairov
 * 
 */
public class NotificationServiceImpl implements NotificationService {

	private Log log = null;

	private Map<Event, Set<Binding>> map = new HashMap<Event, Set<Binding>>();

	/**
	 * Class that encapsulates object and method and ready for invocation
	 * 
	 * @author zubairov
	 * 
	 */
	private static final class Binding {

		private Method method;

		private Object object;

		public Binding(Object object, Method method) {
			if (method == null || object == null)
				throw new NullPointerException("Parameters missing");
			this.method = method;
			this.object = object;
		}

		/**
		 * Notify listener
		 * 
		 * @throws InvocationTargetException
		 * @throws IllegalAccessException
		 * @throws IllegalArgumentException
		 */
		public void invoke(Object[] params) throws IllegalArgumentException,
				IllegalAccessException, InvocationTargetException {
			Class<?>[] types = method.getParameterTypes();
			if (types.length == 0) {
				method.invoke(object, new Object[0]);
			} else {
				if (types.length != params.length) {
					throw new ApplicationRuntimeException(
							"You need to specify following parameters "
									+ Arrays.asList(types)
									+ " to notify method " + method);
				}
				method.invoke(object, params);
			}
		}

		@Override
		public boolean equals(Object arg0) {
			if (arg0 != null && arg0 instanceof Binding) {
				return method.equals(((Binding) arg0).method);
			}
			return super.equals(arg0);
		}

		@Override
		public int hashCode() {
			return method.hashCode();
		}

		@Override
		public String toString() {
			return method.toString();
		}

	}

	/**
	 * {@inheritDoc}
	 */
	public void registerListener(Object listener) {
		Class clazz = listener.getClass();
		if (listener instanceof CommonBaseComponent
				|| listener instanceof CommonBasePage) {
			clazz = clazz.getSuperclass();
		}
		Method[] declaredMethods = clazz.getDeclaredMethods();
		for (int i = 0; i < declaredMethods.length; i++) {
			Method method = declaredMethods[i];
			Listener annotation = method.getAnnotation(Listener.class);
			if (annotation != null) {
				log.debug("Found Listener annotated " + "method "
						+ method.getName() + " that listen for "
						+ Arrays.asList(annotation.value()));
				for (int j = 0; j < annotation.value().length; j++) {
					Event eventType = annotation.value()[j];
					addListener(eventType, new Binding(listener, method));
				}
			}
		}
	}

	/**
	 * Notify event listener with multiple parameters
	 * 
	 * @param event
	 * @param objects
	 */
	public void notifyListeners(Event event, Object... objects) {
		Set<Binding> methods = new HashSet<Binding>();
		Set<Binding> eventListeners = map.get(event);
		Set<Binding> allListeners = map.get(Event.ALL);
		if (eventListeners != null) {
			methods.addAll(eventListeners);
		}
		if (allListeners != null) {
			methods.addAll(allListeners);
		}
		if (methods.size() > 0) {
			for (Iterator iter = methods.iterator(); iter.hasNext();) {
				Binding binding = (Binding) iter.next();
				try {
					binding.invoke(objects);
				} catch (IllegalArgumentException e) {
					log.error("Exception during invocation of the listener", e);
					throw new ApplicationRuntimeException(
							"Parameters do not match", e);
				} catch (IllegalAccessException e) {
					log.error("Exception during invocation of the listener", e);
				} catch (InvocationTargetException e) {
					log.error("Exception during invocation of the listener", e);
					throw new ApplicationRuntimeException(e.getCause());
				}
			}
		} else {
			log.warn("No listeners registered for event " + event);
		}
	}

	/**
	 * Adds a listener to the internal hashmap
	 * 
	 * @param eventType
	 * @param method
	 */
	private void addListener(Event eventType, Binding binding) {
		Set<Binding> bindings = map.get(eventType);
		if (bindings == null) {
			bindings = new HashSet<Binding>();
			map.put(eventType, bindings);
		}
		bindings.add(binding);
	}

	/**
	 * Injector method for the log
	 * 
	 * @param log
	 */
	public void setLog(Log log) {
		this.log = log;
	}
}
